package com.thinkbiganalytics.metadata.modeshape.security;

/*-
 * #%L
 * kylo-metadata-modeshape
 * %%
 * Copyright (C) 2017 - 2018 ThinkBig Analytics, a Teradata Company
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.catalog.Connector;
import com.thinkbiganalytics.metadata.api.catalog.ConnectorProvider;
import com.thinkbiganalytics.metadata.api.catalog.DataSource;
import com.thinkbiganalytics.metadata.api.catalog.DataSourceProvider;
import com.thinkbiganalytics.metadata.api.catalog.security.ConnectorAccessControl;
import com.thinkbiganalytics.metadata.api.category.Category;
import com.thinkbiganalytics.metadata.api.category.CategoryProvider;
import com.thinkbiganalytics.metadata.api.category.security.CategoryAccessControl;
import com.thinkbiganalytics.metadata.api.datasource.Datasource;
import com.thinkbiganalytics.metadata.api.datasource.DatasourceProvider;
import com.thinkbiganalytics.metadata.api.datasource.security.DatasourceAccessControl;
import com.thinkbiganalytics.metadata.api.feed.Feed;
import com.thinkbiganalytics.metadata.api.feed.FeedProvider;
import com.thinkbiganalytics.metadata.api.feed.security.FeedAccessControl;
import com.thinkbiganalytics.metadata.api.project.security.ProjectAccessControl;
import com.thinkbiganalytics.metadata.api.security.RoleNotFoundException;
import com.thinkbiganalytics.metadata.api.template.FeedManagerTemplate;
import com.thinkbiganalytics.metadata.api.template.FeedManagerTemplateProvider;
import com.thinkbiganalytics.metadata.api.template.security.TemplateAccessControl;
import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess;
import com.thinkbiganalytics.metadata.modeshape.catalog.connector.JcrConnector;
import com.thinkbiganalytics.metadata.modeshape.catalog.datasource.JcrDataSource;
import com.thinkbiganalytics.metadata.modeshape.category.JcrCategory;
import com.thinkbiganalytics.metadata.modeshape.common.SecurityPaths;
import com.thinkbiganalytics.metadata.modeshape.datasource.JcrUserDatasource;
import com.thinkbiganalytics.metadata.modeshape.feed.JcrFeed;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedActions;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedEntityActionsProvider;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.modeshape.template.JcrFeedTemplate;
import com.thinkbiganalytics.security.AccessController;
import com.thinkbiganalytics.security.action.Action;
import com.thinkbiganalytics.security.action.AllowableAction;
import com.thinkbiganalytics.security.action.AllowedActions;
import com.thinkbiganalytics.security.role.SecurityRole;
import com.thinkbiganalytics.security.role.SecurityRoleProvider;

import java.security.Principal;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.jcr.Node;

/**
 * Utility methods for setting up entity access control during a fresh installation or when 
 * entity access control is turned on at a later time.
 */
// TODO: Create a builder for defining new entity-level actions and role assignments
public class AccessControlConfigurator {


    @Inject
    private MetadataAccess metadata;
    
    @Inject
    private AccessController accessController;

    @Inject
    private SecurityRoleProvider roleProvider;

    @Inject
    private JcrAllowedEntityActionsProvider actionsProvider;

    @Inject
    private CategoryProvider categoryProvider;

    @Inject
    private FeedProvider feedProvider;
    
    @Inject
    private ConnectorProvider connectorProvider;

    @Inject
    private FeedManagerTemplateProvider feedManagerTemplateProvider;
    
    @Inject
    private DatasourceProvider legacyDatasourceProvider;
    
    @Inject
    private DataSourceProvider dataSourceProvider;

    private volatile boolean entityRolesConfigured = false;

    /**
     * Creates a new role for a particular entity type.  This method must be call within a metadata transaction.
     * @param entityName the name of the entity type (generally from SecurityRole.<entity type>)
     * @param roleName the system name of the role
     * @param title the title of the role
     * @param desc a description of the role
     * @param actions any additional permitted actions allowed by this role
     * @return the new role instance
     */
    public SecurityRole createDefaultRole(String entityName, String roleName, String title, String desc, Action... actions) {
        Supplier<SecurityRole> createIfNotFound = () -> {
            SecurityRole role = roleProvider.createRole(entityName, roleName, title, desc);
            role.setPermissions(actions);
            return role;
        };

        Function<SecurityRole, SecurityRole> ensureActions = (role) -> {
            role.setDescription(desc);
            if (actions != null) {
                List<Action> actionsList = Arrays.asList(actions);
                boolean needsUpdate = actionsList.stream().anyMatch(action -> !role.getAllowedActions().hasPermission(action));
                if (needsUpdate) {
                    role.setPermissions(actions);
                }
            }
            return role;
        };

        try {
            return roleProvider.getRole(entityName, roleName).map(ensureActions).orElseGet(createIfNotFound);

        } catch (RoleNotFoundException e) {
            return createIfNotFound.get();
        }
    }

    /**
     * Creates a new role for a particular entity type; using an existing base role for initial definition of permissions.
     * This method must be call within a metadata transaction.
     * @param entityName the name of the entity type (generally from SecurityRole.<entity type>)
     * @param roleName the system name of the role
     * @param title the title of the role
     * @param desc a description of the role
     * @param baseRole a base role from which to derive this new role's allowed permissions
     * @param actions any additional permitted actions allowed by this role
     * @return the new role instance
     */
    public SecurityRole createDefaultRole(@Nonnull final String entityName, @Nonnull final String roleName, @Nonnull final String title, final String desc, @Nonnull final SecurityRole baseRole,
                                             final Action... actions) {
        final Stream<Action> baseActions = baseRole.getAllowedActions().getAvailableActions().stream().flatMap(AllowableAction::stream);
        final Action[] allowedActions = Stream.concat(baseActions, Stream.of(actions)).toArray(Action[]::new);
        return createDefaultRole(entityName, roleName, title, desc, allowedActions);
    }
  
    public void configureServicesActions() {
        metadata.commit(() -> {
            Node securityNode = JcrUtil.getNode(JcrMetadataAccess.getActiveSession(), SecurityPaths.SECURITY.toString());
            Node svcAllowedNode = JcrUtil.getOrCreateNode(securityNode, AllowedActions.SERVICES, JcrAllowedActions.NODE_TYPE);

            actionsProvider.updateEntityAllowedActions(AllowedActions.SERVICES, svcAllowedNode);
        }, MetadataAccess.SERVICE);
        
    }
    
    public void configureDefaultEntityRoles() {
        if (! this.entityRolesConfigured) {
            this.entityRolesConfigured = metadata.commit(() -> {
                createDefaultRoles();
                createDefaultCatalogRoles();

                if (this.accessController.isEntityAccessControlled()) {
                    ensureTemplateAccessControl();
                    ensureCategoryAccessControl();
                    ensureFeedAccessControl();
                    ensureDataSourceAccessControl();
                    ensureCatalogAccessControl();
                }
                
                return true;
            }, MetadataAccess.SERVICE);
        }
    }

    /**
     * Adds the default roles introduced by the catalog architecture.
     */
    public void createDefaultCatalogRoles() {
        createDefaultRole(SecurityRole.CONNECTOR, "readOnly", "Read-Only", "Allows a user to view information about the connector",
                          ConnectorAccessControl.ACCESS_CONNECTOR);
        createDefaultRole(SecurityRole.CONNECTOR, "admin", "Admin", "Allows a user to edit the connector information plus activate/deactivate it, change its permissions, and create data sources from it",
                          ConnectorAccessControl.ACCESS_CONNECTOR,
                          ConnectorAccessControl.EDIT_CONNECTOR,
                          ConnectorAccessControl.CREATE_DATA_SOURCE,
                          ConnectorAccessControl.ACTIVATE_CONNECTOR,
                          ConnectorAccessControl.CHANGE_PERMS);
        createDefaultRole(SecurityRole.CONNECTOR, "use", "Use", "Allows a user to view information about the connector plus create data sources from it",
                          ConnectorAccessControl.ACCESS_CONNECTOR,
                          ConnectorAccessControl.CREATE_DATA_SOURCE);
    }

    /**
     * Ensures that the entity-level access control is setup up for the entities introduced by the connector architecture.
     */
    public void ensureCatalogAccessControl() {
        List<Connector> connectors = connectorProvider.findAll(true);
        List<SecurityRole> conntorRoles = this.roleProvider.getEntityRoles(SecurityRole.CONNECTOR);
        Optional<AllowedActions> connectorActions = this.actionsProvider.getAvailableActions(AllowedActions.CONNECTOR);
        
        connectors.stream().forEach(conn -> {
            Principal owner = conn.getOwner() != null ? conn.getOwner() : JcrMetadataAccess.getActiveUser();
            connectorActions.ifPresent(actions -> ((JcrConnector) conn).enableAccessControl((JcrAllowedActions) actions, owner, conntorRoles));
        });
        
        List<DataSource> dataSources = dataSourceProvider.findAll();
        List<SecurityRole> dataSourceRoles = this.roleProvider.getEntityRoles(SecurityRole.DATASOURCE);
        Optional<AllowedActions> dataSourceActions = this.actionsProvider.getAvailableActions(AllowedActions.DATASOURCE);

        dataSources.stream()
            .map(JcrDataSource.class::cast)
            .forEach(dataSource -> {
                Principal owner = dataSource.getOwner() != null ? dataSource.getOwner() : JcrMetadataAccess.getActiveUser();
                dataSourceActions.ifPresent(actions -> dataSource.enableAccessControl((JcrAllowedActions) actions, owner, dataSourceRoles));
            });
    }

    private void createDefaultRoles() {
        // Create default roles
        SecurityRole
            feedEditor = createDefaultRole(SecurityRole.FEED, "editor", "Editor", "Allows a user to edit, enable/disable, start, delete and export feed. Allows access to job operations for feed. "
                            + "If role inherited via a category, allows these operations for feeds under that category.",
                                           FeedAccessControl.EDIT_DETAILS,
                                           FeedAccessControl.DELETE,
                                           FeedAccessControl.ACCESS_OPS,
                                           FeedAccessControl.ENABLE_DISABLE,
                                           FeedAccessControl.START,
                                           FeedAccessControl.EXPORT);

        //admin can do everything the editor does + change perms
        createDefaultRole(SecurityRole.FEED, "admin", "Admin", "All capabilities defined in the 'Editor' role along with the ability to change the permissions", feedEditor,
                          FeedAccessControl.CHANGE_PERMS);

        createDefaultRole(SecurityRole.FEED, "readOnly", "Read-Only", "Allows a user to view the feed and access job operations",
                          FeedAccessControl.ACCESS_DETAILS,
                          FeedAccessControl.ACCESS_OPS);

        SecurityRole templateEditor = createDefaultRole(SecurityRole.TEMPLATE, "editor", "Editor", "Allows a user to edit,export a template",
                                                        TemplateAccessControl.ACCESS_TEMPLATE,
                                                        TemplateAccessControl.EDIT_TEMPLATE,
                                                        TemplateAccessControl.DELETE,
                                                        TemplateAccessControl.CREATE_FEED,
                                                        TemplateAccessControl.EXPORT);
        createDefaultRole(SecurityRole.TEMPLATE, "admin", "Admin", "All capabilities defined in the 'Editor' role along with the ability to change the permissions", templateEditor,
                          TemplateAccessControl.CHANGE_PERMS);

        createDefaultRole(SecurityRole.TEMPLATE, "readOnly", "Read-Only", "Allows a user to view the template", TemplateAccessControl.ACCESS_TEMPLATE);

        SecurityRole categoryEditor = createDefaultRole(SecurityRole.CATEGORY, "editor", "Editor", "Allows a user to edit, export and delete category. Allows creating feeds under the category",
                                                        CategoryAccessControl.ACCESS_CATEGORY,
                                                        CategoryAccessControl.EDIT_DETAILS,
                                                        CategoryAccessControl.EDIT_SUMMARY,
                                                        CategoryAccessControl.CREATE_FEED,
                                                        CategoryAccessControl.DELETE);

        createDefaultRole(SecurityRole.CATEGORY, "admin", "Admin", "All capabilities defined in the 'Editor' role along with the ability to change the permissions", categoryEditor,
                          CategoryAccessControl.CHANGE_PERMS);

        createDefaultRole(SecurityRole.CATEGORY, "readOnly", "Read-Only", "Allows a user to view the category", CategoryAccessControl.ACCESS_CATEGORY);

        createDefaultRole(SecurityRole.CATEGORY, "feedCreator", "Feed Creator", "Allows a user to create a new feed using this category", CategoryAccessControl.ACCESS_DETAILS,
                          CategoryAccessControl.CREATE_FEED);

        final SecurityRole datasourceEditor = createDefaultRole(SecurityRole.DATASOURCE, "editor", "Editor", "Allows a user to edit,delete datasources",
                                                                DatasourceAccessControl.ACCESS_DATASOURCE,
                                                                DatasourceAccessControl.EDIT_DETAILS,
                                                                DatasourceAccessControl.EDIT_SUMMARY,
                                                                DatasourceAccessControl.DELETE);
        createDefaultRole(SecurityRole.DATASOURCE, "admin", "Admin", "All capabilities defined in the 'Editor' role along with the ability to change the permissions", datasourceEditor,
                          DatasourceAccessControl.CHANGE_PERMS);
        createDefaultRole(SecurityRole.DATASOURCE, "readOnly", "Read-Only", "Allows a user to view the datasource",
                          DatasourceAccessControl.ACCESS_DATASOURCE,
                          DatasourceAccessControl.ACCESS_DETAILS);

        final SecurityRole projectEditor = createDefaultRole(SecurityRole.PROJECT, ProjectAccessControl.ROLE_EDITOR, "Editor", "Allows a user to edit, delete projects",
                                                             ProjectAccessControl.ACCESS_PROJECT,
                                                             ProjectAccessControl.EDIT_PROJECT,
                                                             ProjectAccessControl.DELETE_PROJECT);
        createDefaultRole(SecurityRole.PROJECT, ProjectAccessControl.ROLE_ADMIN, "Admin", "All capabilities defined in the 'Editor' role along with the ability to change the permissions",
                          projectEditor,
                          ProjectAccessControl.CHANGE_PERMS);
        createDefaultRole(SecurityRole.PROJECT, ProjectAccessControl.ROLE_READER, "Read-Only", "Allows a user to view the project", ProjectAccessControl.ACCESS_PROJECT);
    }

    private void ensureFeedAccessControl() {
        List<Feed> feeds = feedProvider.findAll();
        if (feeds != null) {
            List<SecurityRole> roles = this.roleProvider.getEntityRoles(SecurityRole.FEED);
            Optional<AllowedActions> allowedActions = this.actionsProvider.getAvailableActions(AllowedActions.FEED);
            feeds.stream().forEach(feed -> {
                Principal owner = feed.getOwner() != null ? feed.getOwner() : JcrMetadataAccess.getActiveUser();
                allowedActions.ifPresent(actions -> ((JcrFeed) feed).enableAccessControl((JcrAllowedActions) actions, owner, roles));
            });
        }
    }

    private void ensureCategoryAccessControl() {
        List<Category> categories = categoryProvider.findAll();
        if (categories != null) {
            List<SecurityRole> catRoles = this.roleProvider.getEntityRoles(SecurityRole.CATEGORY);
            List<SecurityRole> feedRoles = this.roleProvider.getEntityRoles(SecurityRole.FEED);

            Optional<AllowedActions> allowedActions = this.actionsProvider.getAvailableActions(AllowedActions.CATEGORY);
            categories.stream().forEach(category -> {
                Principal owner = category.getOwner() != null ? category.getOwner() : JcrMetadataAccess.getActiveUser();
                allowedActions.ifPresent(actions -> ((JcrCategory) category).enableAccessControl((JcrAllowedActions) actions, owner, catRoles, feedRoles));
            });
        }
    }

    private void ensureTemplateAccessControl() {
        List<FeedManagerTemplate> templates = feedManagerTemplateProvider.findAll();
        if (templates != null) {
            List<SecurityRole> roles = this.roleProvider.getEntityRoles(SecurityRole.TEMPLATE);
            Optional<AllowedActions> allowedActions = this.actionsProvider.getAvailableActions(AllowedActions.TEMPLATE);
            templates.stream().forEach(template -> {
                Principal owner = template.getOwner() != null ? template.getOwner() : JcrMetadataAccess.getActiveUser();
                allowedActions.ifPresent(actions -> ((JcrFeedTemplate) template).enableAccessControl((JcrAllowedActions) actions, owner, roles));
            });
        }
    }
    
    private void ensureDataSourceAccessControl() {
        List<Datasource> datasources = legacyDatasourceProvider.getDatasources();
        List<SecurityRole> roles = this.roleProvider.getEntityRoles(SecurityRole.DATASOURCE);
        Optional<AllowedActions> allowedActions = this.actionsProvider.getAvailableActions(AllowedActions.DATASOURCE);

        datasources.stream()
            .filter(JcrUserDatasource.class::isInstance)
            .map(JcrUserDatasource.class::cast)
            .forEach(datasource -> {
                Principal owner = datasource.getOwner() != null ? datasource.getOwner() : JcrMetadataAccess.getActiveUser();
                allowedActions.ifPresent(actions -> datasource.enableAccessControl((JcrAllowedActions) actions, owner, roles));
            });
    }

}
